/*-
 * create by lmx
 * 重写及定制kath过滤器
 */

package com.lmx;

import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Scanner;
import java.util.Set;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.net.MediaType;
import com.lmx.common.constants.CommonConstants;
import com.lmx.common.context.ActionContextHolder;
import com.lmx.kathHack.serialize.PayloadDatas;
import com.lmx.kathHack.serialize.RequestBodyExt;
import com.lmx.repoDomain.exception.BusinessException;
import com.lmx.repoDomain.exception.BusinessExceptionMapper;
import com.lmx.repoDomain.tmodel.BaseDataEntity;
import com.lmx.repoDomain.tmodel.unpersis.ExportParam;
import com.lmx.repoDomain.tmodel.unpersis.SubmitParam;

import io.katharsis.dispatcher.RequestDispatcher;
import io.katharsis.errorhandling.exception.KatharsisMappableException;
import io.katharsis.errorhandling.exception.KatharsisMatchingException;
import io.katharsis.errorhandling.mapper.KatharsisExceptionMapper;
import io.katharsis.invoker.JsonApiMediaType;
import io.katharsis.invoker.KatharsisInvokerException;
import io.katharsis.queryParams.QueryParams;
import io.katharsis.queryParams.QueryParamsBuilder;
import io.katharsis.repository.RepositoryMethodParameterProvider;
import io.katharsis.repository.ResourceRepository;
import io.katharsis.request.dto.DataBody;
import io.katharsis.request.dto.LinkageData;
import io.katharsis.request.dto.RequestBody;
import io.katharsis.request.path.JsonPath;
import io.katharsis.request.path.PathBuilder;
import io.katharsis.resource.field.ResourceAttributesBridge;
import io.katharsis.resource.field.ResourceField;
import io.katharsis.resource.information.ResourceInformation;
import io.katharsis.resource.registry.RegistryEntry;
import io.katharsis.resource.registry.ResourceRegistry;
import io.katharsis.response.BaseResponse;
import io.katharsis.spring.SpringParameterProvider;
import io.katharsis.utils.PropertyUtils;
import io.katharsis.utils.parser.TypeParser;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;

public class KathExtendFilter implements Filter, BeanFactoryAware {
	private static final Logger log = LoggerFactory
			.getLogger(KathExtendFilter.class);

	private static final int BUFFER_SIZE = 4096;
	private final String HTTP_MEHTOD_POST = "POST";
	private final String HTTP_MEHTOD_PATCH = "PATCH";

	private ObjectMapper objectMapper;
	private QueryParamsBuilder queryParamsBuilder;
	private ResourceRegistry resourceRegistry;
	private RequestDispatcher requestDispatcher;
	private String webPathPrefix;

	private ConfigurableBeanFactory beanFactory;

	public KathExtendFilter() {

	}

	public KathExtendFilter(ObjectMapper objectMapper,
			QueryParamsBuilder queryParamsBuilder,
			ResourceRegistry resourceRegistry,
			RequestDispatcher requestDispatcher, String webPathPrefix) {
		this.objectMapper = objectMapper;
		this.queryParamsBuilder = queryParamsBuilder;
		this.resourceRegistry = resourceRegistry;
		this.requestDispatcher = requestDispatcher;
		this.webPathPrefix = webPathPrefix != null ? webPathPrefix : "";
	}

	@Override
	public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
		if (beanFactory instanceof ConfigurableBeanFactory) {
			this.beanFactory = (ConfigurableBeanFactory) beanFactory;
		}
	}

	@Override
	public void init(FilterConfig filterConfig) throws ServletException {

	}

	@Override
	public void doFilter(ServletRequest req, ServletResponse res,
			FilterChain chain) throws IOException, ServletException {
		if (req instanceof HttpServletRequest
				&& res instanceof HttpServletResponse) {
			HttpServletRequest request = (HttpServletRequest) req;
			HttpServletResponse response = (HttpServletResponse) res;
			req.setCharacterEncoding("UTF-8");

			boolean passToFilters = false;
			try {
				passToFilters = invoke(request, response);
			} catch (KatharsisInvokerException e) {
				log.warn("Katharsis Invoker exception.", e);
				response.setStatus(e.getStatusCode());
			} catch (Exception e) {
				throw new ServletException("Katharsis invocation failed.", e);
			}
			if (passToFilters) {
				chain.doFilter(req, res);
			}
		} else {
			chain.doFilter(req, res);
		}
	}

	private boolean invoke(HttpServletRequest request,
			HttpServletResponse response) {
		if (!isAcceptablePath(request)) {
			return true;
		}
		if (isAcceptableMediaType(request)) {
			try {
				return dispatchRequest(request, response);
			} catch (Exception e) {
				log.error("dispatchRequest fail", e);
				throw new KatharsisInvokerException(
						HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e);
			}
		} else {
			return true;
		}
	}

	private boolean dispatchRequest(HttpServletRequest request,
			HttpServletResponse response) throws Exception {
		BaseResponse<?> katharsisResponse = null;

		boolean passToFilters = false;

		InputStream in = null;
		String[] ignoreLinks = null;
		try {
			JsonPath jsonPath = new PathBuilder(resourceRegistry)
					.buildPath(getRequestPath(request));
			log.debug("jsonPath:" + jsonPath.getElementName());
			// 忽略link信息段
			ignoreLinks = this.createIgnoreLinkParam(request);
			// 导出信息段
			this.createExportParam(request);
			QueryParams queryParams = createQueryParams(request);
			RepositoryMethodParameterProvider parameterProvider = new SpringParameterProvider(
					beanFactory, request);
			in = request.getInputStream();
			RequestBody requestBody = inputStreamToBody(in);
			if (requestBody != null) {
				// 检查是否有payload内容，有则放到安全线程中
				PayloadDatas payloadDatas = ((RequestBodyExt) requestBody)
						.getSingleData().getPostPayload();
				if (payloadDatas != null
						&& payloadDatas.getAdditionalProperties() != null) {
					String resourceEndpointName = jsonPath.getResourceName();
					RegistryEntry endpointRegistryEntry = resourceRegistry
							.getEntry(resourceEndpointName);
					Map<String, List<Object>> map = new HashMap<String, List<Object>>();
					Iterator<String> it = payloadDatas
							.getAdditionalProperties().keySet().iterator();
					// 循环遍历每一个payload内容，转换为实体对象
					while (it.hasNext()) {
						String key = it.next();
						List<Object> entities = new ArrayList<Object>();
						Object payloadData = payloadDatas
								.getAdditionalProperties().get(key);
						if ("reverse".equals(key)) {
							String value = (String) payloadData;
							entities.add(value);
						} else if (List.class.isInstance(payloadData)) {
							List<Object> dataBodys = (List<Object>) payloadDatas
									.getAdditionalProperties().get(key);
							Iterator dit = dataBodys.iterator();
							// 每一个key对应一个实体列表
							while (dit.hasNext()) {
								DataBody dataBody = (DataBody) dit.next();
								Object newResource = transToBaseDataEntity(
										queryParams, parameterProvider,
										endpointRegistryEntry, dataBody);
								entities.add(newResource);
							}
						} else {
							DataBody dataBody = (DataBody) payloadData;
							Object newResource = transToBaseDataEntity(
									queryParams, parameterProvider,
									endpointRegistryEntry, dataBody);
							entities.add(newResource);
						}

						map.put(key, entities);
					}
					ActionContextHolder.getContext().setPayloadDatas(map);
				}
			}
			String method = request.getMethod();
			if (this.checkRepeatSubmit(request, jsonPath, method)) {
				throw new BusinessException("",
						CommonConstants.ERROR_CODE.REPEAT_SUBMIT_ERROR.label(),
						"");
			}
			katharsisResponse = requestDispatcher.dispatchRequest(jsonPath,
					method, queryParams, parameterProvider, requestBody);
		} catch (BusinessException e) {
			katharsisResponse = new BusinessExceptionMapper()
					.toErrorResponse(e);
		} catch (KatharsisMappableException e) {
			if (log.isDebugEnabled()) {
				log.warn("Error occurred while dispatching katharsis request. "
						+ e, e);
			} else {
				log.warn("Error occurred while dispatching katharsis request. "
						+ e);
			}
			katharsisResponse = new KatharsisExceptionMapper()
					.toErrorResponse(e);
		} catch (KatharsisMatchingException e) {
			// We can pass it only in case of requests where no input stream is
			// used
			if (isPassableMethod(request)) {
				passToFilters = true;
			} else {
				response.setStatus(HttpServletResponse.SC_NOT_FOUND);
			}
		} finally {
			if (!passToFilters) {
				closeQuietly(in);

				if (katharsisResponse != null) {
					response.setStatus(katharsisResponse.getHttpStatus());
					response.setContentType(JsonApiMediaType.APPLICATION_JSON_API);

					ByteArrayOutputStream baos = null;
					OutputStream out = null;
					out = response.getOutputStream();
					try {
						// first write to a buffer first because objectMapper
						// may fail while writing.
						baos = new ByteArrayOutputStream(BUFFER_SIZE);
						objectMapper.writeValue(baos, katharsisResponse);
						// 处理需要忽略的链接属性
						if (ignoreLinks != null) {
							String ret = baos.toString();
							JSONObject o = JSONObject.fromObject(ret);
							Object datas = o.get("data");
							if (datas instanceof JSONArray) {
								JSONArray dataArray = (JSONArray) datas;
								for (int i = 0; i < dataArray.size(); i++) {
									JSONObject data = (JSONObject) dataArray
											.get(i);
									JSONObject rel = data
											.getJSONObject("relationships");
									for (int j = 0; j < ignoreLinks.length; j++) {
										rel.remove(ignoreLinks[j]);
									}
								}
							} else {
								JSONObject data = (JSONObject) datas;
								JSONObject rel = data
										.getJSONObject("relationships");
								for (int j = 0; j < ignoreLinks.length; j++) {
									rel.remove(ignoreLinks[j]);
								}
							}
							out.write(o.toString().getBytes());
						} else {
							out.write(baos.toByteArray());
						}
						out.flush();
					} finally {
						closeQuietly(baos);
						closeQuietly(out);
					}
				} else {
					response.setStatus(HttpServletResponse.SC_NO_CONTENT);
				}
			}
		}
		return passToFilters;
	}

	/**
	 * 判断是否重复提交请求
	 *
	 * @author ytche
	 * @param request
	 * @param jsonPath
	 * @param method
	 * @date 2017年7月26日 上午10:43:25
	 */
	private Boolean checkRepeatSubmit(HttpServletRequest request,
			JsonPath jsonPath, String method) {
		Boolean rtn = false;
		String ignoreElementStr = request.getHeader("dur-noprevent");
		if (StringUtils.isNotBlank(ignoreElementStr)) {
			return rtn;
		}
		List<String> ignoreElements = Lists.newArrayList("customerPreference",
				"chargingStandard", "marketSkill", "role",
				"evaluateexameranswer", "evaluateresult");
		if (ignoreElements.contains(jsonPath.getElementName())) {
			return rtn;
		}
		if (!method.equals(this.HTTP_MEHTOD_POST)
				&& !method.equals(this.HTTP_MEHTOD_PATCH)) {
			return rtn;
		}
		String token = request.getHeader("ccd-token");
		if (org.apache.commons.lang3.StringUtils.isBlank(token)) {
			return rtn;
		}
		Map<String, SubmitParam> paramMap = ActionContextHolder
				.getSubmitParams();
		rtn = paramMap.containsKey(token)
				&& jsonPath.getElementName().equals(
						paramMap.get(token).getElementName());
		if (rtn) {
			Long time = System.currentTimeMillis();
			Long LastTime = paramMap.get(token).getSubmitTime();
			// 五秒内判定为重复提交
			rtn = rtn && (time - LastTime <= 5000);
		}
		// 重复判定不会更新最后提交时间
		if (!rtn) {
			SubmitParam param = new SubmitParam();
			param.setElementName(jsonPath.getElementName());
			param.setSubmitTime(System.currentTimeMillis());
			ActionContextHolder.putSubmitParams(token, param);
		}
		return rtn;
	}

	/**
	 * 把playload的数据转成entity
	 *
	 * @author ytche
	 * @throws IOException
	 * @date 2017年5月24日 下午12:22:40
	 */
	private Object transToBaseDataEntity(QueryParams queryParams,
			RepositoryMethodParameterProvider parameterProvider,
			RegistryEntry endpointRegistryEntry, DataBody dataBody)
			throws InstantiationException, IllegalAccessException,
			NoSuchMethodException, InvocationTargetException, IOException {
		RegistryEntry bodyRegistryEntry = resourceRegistry.getEntry(dataBody
				.getType());
		Object newResource = bodyRegistryEntry.getResourceInformation()
				.getResourceClass().newInstance();
		if (dataBody.getId() != null && !"".equals(dataBody.getId())) {
			((BaseDataEntity) newResource).setId(Long.parseLong(dataBody
					.getId()));
		}
		setAttributes(dataBody, newResource,
				bodyRegistryEntry.getResourceInformation());
		ResourceRepository resourceRepository = endpointRegistryEntry
				.getResourceRepository(parameterProvider);
		setRelations(newResource, bodyRegistryEntry, dataBody, queryParams,
				parameterProvider);
		return newResource;
	}

	private boolean isPassableMethod(HttpServletRequest request) {
		List<HttpMethod> passableMethods = Arrays.asList(HttpMethod.GET,
				HttpMethod.OPTIONS, HttpMethod.HEAD, HttpMethod.TRACE);
		for (HttpMethod passableMethod : passableMethods) {
			if (passableMethod.name().equalsIgnoreCase(request.getMethod())) {
				return true;
			}
		}

		return false;
	}

	private boolean isAcceptablePath(HttpServletRequest request) {
		String contextPath = request.getContextPath();
		if (contextPath.startsWith("/") && contextPath.length() == 1) {
			contextPath = "";
		}
		return request.getRequestURI().startsWith(contextPath + webPathPrefix);
	}

	private String getRequestPath(HttpServletRequest request) {
		String path = request.getPathInfo();

		// Serving with Filter, pathInfo can be null.
		if (path == null) {
			path = request.getRequestURI().substring(
					request.getContextPath().length() + webPathPrefix.length());
		}

		return path;
	}

	private boolean isAcceptableMediaType(HttpServletRequest servletRequest) {
		String acceptHeader = servletRequest.getHeader(HttpHeaders.ACCEPT);

		if (acceptHeader != null) {
			String[] accepts = acceptHeader.split(",");
			MediaType acceptableType;

			for (String mediaTypeItem : accepts) {
				try {
					acceptableType = MediaType.parse(mediaTypeItem.trim());

					if (JsonApiMediaType.isCompatibleMediaType(acceptableType)) {
						return true;
					}
				} catch (Exception e) {
					continue;
				}
			}
		}

		return false;
	}

	/**
	 * This method return a list o parameters. It uses
	 * {@link ServletRequest#getParameterMap()} which can also return POST body
	 * parameters, but we don't expect to receive such body.
	 *
	 * @param request
	 *            request body
	 * @return query parameters
	 */
	private QueryParams createQueryParams(HttpServletRequest request) {
		Map<String, String[]> params = request.getParameterMap();

		Map<String, Set<String>> queryParameters = new HashMap<>(params.size());
		for (Map.Entry<String, String[]> entry : params.entrySet()) {
			queryParameters.put(entry.getKey(),
					new HashSet<>(Arrays.asList(entry.getValue())));
		}
		return queryParamsBuilder.buildQueryParams(queryParameters);
	}

	private RequestBody inputStreamToBody(InputStream is) throws IOException {
		if (is == null) {
			return null;
		}

		Scanner s = new Scanner(is).useDelimiter("\\A");
		String requestBody = s.hasNext() ? s.next() : "";

		if (requestBody == null || requestBody.isEmpty()) {
			return null;
		}

		return objectMapper.readValue(requestBody, RequestBodyExt.class);
	}

	private void closeQuietly(Closeable closeable) {
		if (closeable != null) {
			try {
				closeable.close();
			} catch (IOException ignore) {
			}
		}
	}

	@Override
	public void destroy() {

	}

	void setAttributes(DataBody dataBody, Object instance,
			ResourceInformation resourceInformation)
			throws IllegalAccessException, NoSuchMethodException,
			InvocationTargetException, InstantiationException, IOException {
		if (dataBody.getAttributes() != null) {
			ResourceAttributesBridge resourceAttributesBridge = resourceInformation
					.getAttributeFields();
			resourceAttributesBridge.setProperties(objectMapper, instance,
					dataBody.getAttributes());
		}
	}

	protected void setRelations(Object newResource,
			RegistryEntry registryEntry, DataBody dataBody,
			QueryParams queryParams,
			RepositoryMethodParameterProvider parameterProvider)
			throws NoSuchMethodException, IllegalAccessException,
			InvocationTargetException, InstantiationException {
		if (dataBody.getRelationships() != null) {
			Map<String, Object> additionalProperties = dataBody
					.getRelationships().getAdditionalProperties();
			for (Map.Entry<String, Object> property : additionalProperties
					.entrySet()) {
				setRelationField(newResource, registryEntry,
						(Map.Entry) property, queryParams, parameterProvider);
			}
		}
	}

	private void setRelationField(Object newResource,
			RegistryEntry registryEntry,
			Map.Entry<String, LinkageData> property, QueryParams queryParams,
			RepositoryMethodParameterProvider parameterProvider) {

		ResourceField relationshipFieldByName = registryEntry
				.getResourceInformation().findRelationshipFieldByName(
						property.getKey());

		Object relationObject;
		// 挂接关联关系，如果没有id则不执行
		if (property.getValue() != null && property.getValue().getId() != null) {
			RegistryEntry entry = resourceRegistry
					.getEntry(relationshipFieldByName.getType());
			Class idFieldType = entry.getResourceInformation().getIdField()
					.getType();
			TypeParser typeParser = new TypeParser();
			Serializable castedRelationshipId = typeParser.parse(property
					.getValue().getId(), idFieldType);
			relationObject = entry.getResourceRepository(parameterProvider)
					.findOne(castedRelationshipId, queryParams);
		} else {
			relationObject = null;
		}

		PropertyUtils.setProperty(newResource,
				relationshipFieldByName.getUnderlyingName(), relationObject);

	}

	/**
	 * 如果请求中含有忽略关联的参数,在此处理
	 *
	 * @author lmx
	 * @param request
	 * @date 2017年6月12日
	 */
	private String[] createIgnoreLinkParam(HttpServletRequest request) {
		Map<String, String[]> rawParms = request.getParameterMap();
		String reg = "^ignoreLink.*";
		String formatReg = "^export.*format.*";
		String columnReg = "^export.*cols.*";
		Boolean isLink = false;
		ExportParam exportParam = new ExportParam();
		Map<String, String> formatMap = Maps.newHashMap();
		Map<String, String> columnMap = Maps.newHashMap();
		String[] igLinks = null;
		for (Entry<String, String[]> entry : rawParms.entrySet()) {
			String key = entry.getKey();
			if (key.matches(reg)) {
				isLink = true;
				igLinks = entry.getValue();
			}
		}
		return igLinks;

	}

	/**
	 * 如果请求中含有导出参数,处理请求中的导出参数
	 *
	 * @author ytche
	 * @param request
	 * @date 2017年5月9日 上午10:12:45
	 */
	private void createExportParam(HttpServletRequest request) {
		Map<String, String[]> rawParms = request.getParameterMap();
		System.out.println(rawParms);
		String reg = "^export.*";
		String formatReg = "^export.*format.*";
		String columnReg = "^export.*cols.*";
		Boolean isExport = false;
		ExportParam exportParam = new ExportParam();
		Map<String, String> formatMap = Maps.newHashMap();
		Map<String, String> columnMap = Maps.newHashMap();
		for (Entry<String, String[]> entry : rawParms.entrySet()) {
			String key = entry.getKey();
			if (key.matches(reg)) {
				isExport = true;
				if (key.equals("export[title]")) {
					exportParam.setTitle(entry.getValue()[0]);
					continue;
				}
				if (key.matches(columnReg)) {
					columnMap.put(key, entry.getValue()[0]);
					continue;
				}
				if (key.matches(formatReg)) {
					formatMap.put(key, entry.getValue()[0]);
					continue;
				}
			}
		}
		if (isExport) {
			int formatNum = formatMap.size() / 2;
			for (int i = 0; i < formatNum; i++) {
				String colName = "export[format][" + i + "][colName]";
				String format = "export[format][" + i + "][format]";
				exportParam.putFormat(formatMap.get(colName),
						formatMap.get(format));
			}
			int colNum = columnMap.size() / 2;
			for (int i = 0; i < colNum; i++) {
				String name = "export[cols][" + i + "][name]";
				String title = "export[cols][" + i + "][title]";
				exportParam
						.putColumn(columnMap.get(name), columnMap.get(title));
			}
			ActionContextHolder.getContext().setExportParam(exportParam);
		} else {
			ActionContextHolder.getContext().setExportParam(null);
		}
	}

}